<?php

namespace esp\core;

class Mistake
{
    private $dispatcher;

    private $_root_path;
    private $_font_size;
    private $_show_route;

    public function __construct(Dispatcher $dispatcher, $display = 0, callable $callback = null)
    {
        $this->dispatcher = $dispatcher;
        $this->callback($callback);
        $this->_show_route = true;
        $this->_font_size = '100%';
        $this->_root_path = _ROOT;
        $this->register_handler($display);
    }

    /**
     * 构造回调，主要用于发送通知，或记录
     * @param $object
     */
    private function callback($object)
    {
        static $obj;
        if (is_callable($object)) {
            $obj = $object;
            return;
        }
        if (is_callable($obj)) $obj($object);
    }

    /**
     *
     * 产生一个错误信息，具体处理，由\plugins\Mistake处理
     * @param $str
     * @param int $level 错误级别，012，
     *
     * 0：系统停止执行，严重级别
     * 1：提示错误，继续运行
     * 2：警告级别，在生产环境中不提示，仅发给管理员
     *
     * error("{$filePath} 不是有效文件。");
     */
    public static function try_error($str, $level = 0, $trace = null)
    {
        if ($level < 0) $level = 0;
        if ($level > 2) $level = 2;
        $level = 256 << $level;
        if (is_string($str)) {
            $err = $trace ?: debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
            unset($err['function']);
            $err['error'] = $str;
            $err['code'] = $level;
            $str = json_encode($err, 256);
        }
        //产生一个用户级别的 error/warning/notice 信息
        trigger_error($str, $level);
        exit;
    }


    /**
     * @param $display
     * //显示程度:0=不显示,1=简单,2=完整
     */
    private function register_handler($display)
    {
        /**
         * 一般警告错误
         * @param $errNo
         * @param $errStr
         * @param $errFile
         * @param $errLine
         */
        $handler_warn = function ($errNo, $errStr, $errFile, $errLine) use ($display) {
            $err = [];
            if (in_array($errNo, [256, 512, 1024])) {
                $err = json_decode($errStr, 256);
            } else {
                $err['error'] = $errStr;
                $err['code'] = $errNo;
                $err['file'] = $errFile;
                $err['line'] = $errLine;
            }
            if (!$err) return;
            $this->callback($err);
            if (is_numeric($display)) {
                $display = intval($display);
                if ($display === 1) {
                    print_r($err);
                    exit;
                } else if ($display === 2) {
                    $this->displayError('warn', $err, debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS));
                } else {
                    $this->displayState($display);
                }
            } else {
                exit($display);
            }
        };

        /**
         * 严重错误
         * @param $error
         */
        $handler_error = function ($error) use ($display) {
            $error instanceof \Throwable and 1;
            $err = [];
            $err['error'] = $error->getMessage();
            $err['code'] = $error->getCode();
            $err['file'] = $error->getFile();
            $err['line'] = $error->getLine();
            $this->callback($err);
            if ($display === 1) {
                print_r($err);
                exit;
            } else if ($display === 2) {
                $this->displayError('error', $err, $error->getTrace());
            } else {
                $this->displayState($display);
            }
        };

        /**
         * 注册出错时的处理方法，等同于set_error_handler($handler_error)
         * 处理类型：
         * 1，框架自身出错；
         * 2，PHP原生错误比如：除以0，语法错误等；
         * 3，程序中error()抛出的错误；
         * 4，找不到控制器，找不到控制动作等；
         */
        set_error_handler($handler_warn);

        /**
         * 注册【异常】处理方法，
         * 处理类型：
         * 1，调用了不存在的函数；
         * 2，函数参数不对；
         * 3，throw new \Exception抛出的异常
         */
        set_exception_handler($handler_error);
    }

    /**
     * 显示成一个错误状态
     * @param $code
     */
    public static function displayState($code)
    {
        $state = self::states($code);
        $server = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : null;
        $html = "<html>\n\n<head><title>{$code} {$state}</title></head>\n<body bgcolor=\"white\">\n<center><h1>{$code} {$state}</h1></center>\n<hr><center>{$server}</center>\n</body>\n</html>";
        if (!stripos(PHP_SAPI, 'cgi')) {
            header("Status: {$code} {$state}", true);
        } else {
            $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
            header("{$protocol} {$code} {$state}", true, $code);
        }
        header('Content-type:text/html', true);
        exit($html);
    }

    private function duo2tow(&$array)
    {
        foreach ($array as $i => &$arr) {
            if (is_array($arr)) $arr = json_encode($arr, 256);
        }
    }


    /**
     * 显示并停止所有操作
     * @param $type
     * @param $err
     * @param $trace
     */
    private function displayError($type, $err, $trace)
    {
        if (_CLI) {
            echo "\n\e[40;31;m================ERROR=====================\e[0m\n";
            print_r($err);
            if (!empty($trace)) print_r($trace);
            exit;
        }

        if (is_numeric($err['error'])) {
            $this->displayState($err['error']);
            return;
        }
        $traceHtml = '';
        foreach (array_reverse($trace) as $tr) {
            $str = '<tr><td class="l">';
            if (isset($tr['file'])) $str .= $this->filter_root($tr['file']);
            if (isset($tr['line'])) $str .= " ({$tr['line']})";
            $str .= '</td><td>';

            if (isset($tr['class'])) $str .= $tr['class'];
            if (isset($tr['type'])) $str .= $tr['type'];
            if (isset($tr['function'])) {
                if (empty($tr['args'])) {
                    $args = null;
                } else {
                    $this->duo2tow($tr['args']);
                    $args = '"' . implode('","', $tr['args']) . '"';
                }
                $str .= "{$tr['function']}({$args})";
            }
            $str .= '</td></tr>';
            $traceHtml .= $str;
        }

        if ($this->_show_route) {
            $route = $this->get_routes_info();
            if (!empty($route)) {
                $Params = empty($route['Params']) ? '' : (implode(',', $route['Params']));
                $mca_name = "{$route['Router']} : {$route['Module']} / {$route['Control']} / {$route['Action']}";
                $mca_file = $route['Path'] . $route['ModulePath'] . $route['Control'] . '->' . $route['Action'] . '(' . $Params . ')';
                $routeHtml = '<tr><td class="l">路由结果：</td><td>' . $mca_name . '</td></tr><tr><td class="l">路由请求：</td><td>';
                $routeHtml .= $this->filter_root($mca_file) . '</td></tr>';
                $err['route_name'] = $mca_name;
                $err['route_mca'] = $mca_file;
            } else {
                $routeHtml = '';
            }
        } else {
            $routeHtml = '';
        }

        $fontSize = $this->_font_size ?: '75%';
        if (is_numeric($fontSize)) $fontSize .= ($fontSize > 50 ? '%' : 'px');
        $color = ['#555', '#def', '#ffe', '#f0c040'];
        $html = <<<HTML
<!DOCTYPE html><html lang="zh-cn"><head>
<meta charset="UTF-8"><title>%s</title>
<style>
body {margin: 0;padding: 0;font-size: %s;color:{$color[0]};font-family:"Source Code Pro", "Arial", "Microsoft YaHei", "msyh", "sans-serif";}
table {width: 80%%;margin: 1em auto;border: 1px solid #456;box-shadow: 5px 5px 2px #ccc;}
tr,td {overflow: hidden;}
td {text-indent: 0.5em;line-height: 2em;}
table.head {background: {$color[1]};}table.head td.l {width: 6em;font-weight: bold;}td.msg{color:red;}
table.trade tr:nth-child(odd){background: {$color[2]};} 
table.trade tr.nav{background: {$color[3]};} td.time{text-align: right;padding-right:1em;}
table.trade td {border-bottom: 1px solid #abc;}table.trade td.l {width: 40%%;}</style>
</head><body>
<table class="head" cellpadding="0" cellspacing="0">
<tr><td class="l">错误代码：</td><td>%s</td></tr>
<tr><td class="l">错误信息：</td><td class="msg">%s</td></tr>
<tr><td class="l">错误位置：</td><td>%s</td></tr>%s
</table><table class="trade" cellpadding="0" cellspacing="0">
<tr class="nav"><td><b>Trace</b> : (执行顺序从上往下)</td><td class="time">%s</td></tr>%s</table></body></html>
HTML;
        $html = printf($html,
            $this->filter_root($err['error']),
            $fontSize,
            $type . '=' . $err['code'],
            $this->filter_root($err['error']),
            "{$this->filter_root($err['file'])} ({$err['line']})",
            $routeHtml,
            date('Y-m-d H:i:s'),
            $traceHtml
        );
        exit($html);
    }

    private function filter_root($str)
    {
        return str_replace($this->_root_path, '', $str);
    }


    private function get_routes_info()
    {
        $request = $this->dispatcher->getRequest();
        $route = [];
        $route['Path'] = $request->directory;
        $route['Module'] = $request->module;
        if (empty($route['Module'])) return null;

        $route['Control'] = $request->controller;
        $route['Action'] = $request->action;
        $route['Params'] = $request->getParams();
        $route['Router'] = $request->route;
        if ($route['Module'] === 'www') {
            $route['ModulePath'] = 'controllers/';
        } else {
            $route['ModulePath'] = "{$route['Module']}/controllers/";
        }
        return $route;
    }


    public static function states($code)
    {
        switch (intval($code)) {
            case 200:
                return 'OK';
            case 201:
                return 'Created';
            case 202:
                return 'Accepted';
            case 203:
                return 'Non-Authoritative Information';
            case 204:
                return 'Not Content';
            case 205:
                return 'Reset Content';
            case 206:
                return 'Partial Content';
            case 300:
                return 'Multiple Choices';
            case 301:
                return 'Moved Permanently';
            case 302:
                return 'Found';
            case 303:
                return 'See Other';
            case 304:
                return 'Not Modified';
            case 305:
                return 'Use Proxy';
            case 307:
                return 'Temporary Redirect';
            case 400:
                return 'Bad Request.';
            case 401:
                return 'Unauthorized';
            case 403:
                return 'Forbidden';
            case 404:
                return 'Not Found';
            case 405:
                return 'Method Not Allowed';
            case 406:
                return 'Not Acceptable';
            case 407:
                return 'Proxy Authentication Required';
            case 408:
                return 'Request Timeout';
            case 409:
                return 'Conflict';
            case 410:
                return 'Gone';
            case 411:
                return 'Length Required';
            case 412:
                return 'Precondition Failed';
            case 413:
                return 'Request Entity Too Large';
            case 414:
                return 'Request-URI Too Long';
            case 415:
                return 'Unsupported Media Type';
            case 416:
                return 'Requested Range Not Satisfiable';
            case 417:
                return 'Expectation Failed';
            case 422:
                return 'Unprocessable Entity';
            case 500:
                return 'Internal Server Error';
            case 501:
                return 'Not Implemented';
            case 502:
                return 'Bad Gateway';
            case 503:
                return 'Service Unavailable';
            case 504:
                return 'Gateway Timeout';
            case 505:
                return 'HTTP Version Not Supported';
            default:
                return null;
        }
    }

}